/*! 
 * jquery.event.drag - v 2.2
 * Copyright (c) 2010 Three Dub Media - http://threedubmedia.com
 * Open Source MIT License - http://threedubmedia.com/code/license
 */
// Created: 2008-06-04 
// Updated: 2012-05-21
// REQUIRES: jquery 1.7.x

;
(function ($) {

// add the jquery instance method
  $.fn.drag = function (str, arg, opts) {
    // figure out the event type
    var type = typeof str == "string" ? str : "",
    // figure out the event handler...
      fn = $.isFunction(str) ? str : $.isFunction(arg) ? arg : null;
    // fix the event type
    if (type.indexOf("drag") !== 0)
      type = "drag" + type;
    // were options passed
    opts = ( str == fn ? arg : opts ) || {};
    // trigger or bind event handler
    return fn ? this.bind(type, opts, fn) : this.trigger(type);
  };

// local refs (increase compression)
  var $event = $.event,
    $special = $event.special,
// configure the drag special event 
    drag = $special.drag = {

      // these are the default settings
      defaults: {
        which: 1, // mouse button pressed to start drag sequence
        distance: 0, // distance dragged before dragstart
        not: ':input', // selector to suppress dragging on target elements
        handle: null, // selector to match handle target elements
        relative: false, // true to use "position", false to use "offset"
        drop: true, // false to suppress drop events, true or selector to allow
        click: false // false to suppress click events after dragend (no proxy)
      },

      // the key name for stored drag data
      datakey: "dragdata",

      // prevent bubbling for better performance
      noBubble: true,

      // count bound related events
      add: function (obj) {
        // read the interaction data
        var data = $.data(this, drag.datakey),
        // read any passed options
          opts = obj.data || {};
        // count another realted event
        data.related += 1;
        // extend data options bound with this event
        // don't iterate "opts" in case it is a node
        $.each(drag.defaults, function (key, def) {
          if (opts[ key ] !== undefined)
            data[ key ] = opts[ key ];
        });
      },

      // forget unbound related events
      remove: function () {
        $.data(this, drag.datakey).related -= 1;
      },

      // configure interaction, capture settings
      setup: function () {
        // check for related events
        if ($.data(this, drag.datakey))
          return;
        // initialize the drag data with copied defaults
        var data = $.extend({ related: 0 }, drag.defaults);
        // store the interaction data
        $.data(this, drag.datakey, data);
        // bind the mousedown event, which starts drag interactions
        $event.add(this, "touchstart mousedown", drag.init, data);
        // prevent image dragging in IE...
        if (this.attachEvent)
          this.attachEvent("ondragstart", drag.dontstart);
      },

      // destroy configured interaction
      teardown: function () {
        var data = $.data(this, drag.datakey) || {};
        // check for related events
        if (data.related)
          return;
        // remove the stored data
        $.removeData(this, drag.datakey);
        // remove the mousedown event
        $event.remove(this, "touchstart mousedown", drag.init);
        // enable text selection
        drag.textselect(true);
        // un-prevent image dragging in IE...
        if (this.detachEvent)
          this.detachEvent("ondragstart", drag.dontstart);
      },

      // initialize the interaction
      init: function (event) {
        // sorry, only one touch at a time
        if (drag.touched)
          return;
        // the drag/drop interaction data
        var dd = event.data, results;
        // check the which directive
        if (event.which != 0 && dd.which > 0 && event.which != dd.which)
          return;
        // check for suppressed selector
        if ($(event.target).is(dd.not))
          return;
        // check for handle selector
        if (dd.handle && !$(event.target).closest(dd.handle, event.currentTarget).length)
          return;

        drag.touched = event.type == 'touchstart' ? this : null;
        dd.propagates = 1;
        dd.mousedown = this;
        dd.interactions = [ drag.interaction(this, dd) ];
        dd.target = event.target;
        dd.pageX = event.pageX;
        dd.pageY = event.pageY;
        dd.dragging = null;
        // handle draginit event...
        results = drag.hijack(event, "draginit", dd);
        // early cancel
        if (!dd.propagates)
          return;
        // flatten the result set
        results = drag.flatten(results);
        // insert new interaction elements
        if (results && results.length) {
          dd.interactions = [];
          $.each(results, function () {
            dd.interactions.push(drag.interaction(this, dd));
          });
        }
        // remember how many interactions are propagating
        dd.propagates = dd.interactions.length;
        // locate and init the drop targets
        if (dd.drop !== false && $special.drop)
          $special.drop.handler(event, dd);
        // disable text selection
        drag.textselect(false);
        // bind additional events...
        if (drag.touched)
          $event.add(drag.touched, "touchmove touchend", drag.handler, dd);
        else
          $event.add(document, "mousemove mouseup", drag.handler, dd);
        // helps prevent text selection or scrolling
        if (!drag.touched || dd.live)
          return false;
      },

      // returns an interaction object
      interaction: function (elem, dd) {
        var offset = $(elem)[ dd.relative ? "position" : "offset" ]() || { top: 0, left: 0 };
        return {
          drag: elem,
          callback: new drag.callback(),
          droppable: [],
          offset: offset
        };
      },

      // handle drag-releatd DOM events
      handler: function (event) {
        // read the data before hijacking anything
        var dd = event.data;
        // handle various events
        switch (event.type) {
          // mousemove, check distance, start dragging
          case !dd.dragging && 'touchmove':
            event.preventDefault();
          case !dd.dragging && 'mousemove':
            //  drag tolerance, x≤ + y≤ = distance≤
            if (Math.pow(event.pageX - dd.pageX, 2) + Math.pow(event.pageY - dd.pageY, 2) < Math.pow(dd.distance, 2))
              break; // distance tolerance not reached
            event.target = dd.target; // force target from "mousedown" event (fix distance issue)
            drag.hijack(event, "dragstart", dd); // trigger "dragstart"
            if (dd.propagates) // "dragstart" not rejected
              dd.dragging = true; // activate interaction
          // mousemove, dragging
          case 'touchmove':
            event.preventDefault();
          case 'mousemove':
            if (dd.dragging) {
              // trigger "drag"
              drag.hijack(event, "drag", dd);
              if (dd.propagates) {
                // manage drop events
                if (dd.drop !== false && $special.drop)
                  $special.drop.handler(event, dd); // "dropstart", "dropend"
                break; // "drag" not rejected, stop
              }
              event.type = "mouseup"; // helps "drop" handler behave
            }
          // mouseup, stop dragging
          case 'touchend':
          case 'mouseup':
          default:
            if (drag.touched)
              $event.remove(drag.touched, "touchmove touchend", drag.handler); // remove touch events
            else
              $event.remove(document, "mousemove mouseup", drag.handler); // remove page events
            if (dd.dragging) {
              if (dd.drop !== false && $special.drop)
                $special.drop.handler(event, dd); // "drop"
              drag.hijack(event, "dragend", dd); // trigger "dragend"
            }
            drag.textselect(true); // enable text selection
            // if suppressing click events...
            if (dd.click === false && dd.dragging)
              $.data(dd.mousedown, "suppress.click", new Date().getTime() + 5);
            dd.dragging = drag.touched = false; // deactivate element
            break;
        }
      },

      // re-use event object for custom events
      hijack: function (event, type, dd, x, elem) {
        // not configured
        if (!dd)
          return;
        // remember the original event and type
        var orig = { event: event.originalEvent, type: event.type },
        // is the event drag related or drog related?
          mode = type.indexOf("drop") ? "drag" : "drop",
        // iteration vars
          result, i = x || 0, ia, $elems, callback,
          len = !isNaN(x) ? x : dd.interactions.length;
        // modify the event type
        event.type = type;
        // remove the original event
        event.originalEvent = null;
        // initialize the results
        dd.results = [];
        // handle each interacted element
        do if (ia = dd.interactions[ i ]) {
          // validate the interaction
          if (type !== "dragend" && ia.cancelled)
            continue;
          // set the dragdrop properties on the event object
          callback = drag.properties(event, dd, ia);
          // prepare for more results
          ia.results = [];
          // handle each element
          $(elem || ia[ mode ] || dd.droppable).each(function (p, subject) {
            // identify drag or drop targets individually
            callback.target = subject;
            // force propagtion of the custom event
            event.isPropagationStopped = function () {
              return false;
            };
            // handle the event
            result = subject ? $event.dispatch.call(subject, event, callback) : null;
            // stop the drag interaction for this element
            if (result === false) {
              if (mode == "drag") {
                ia.cancelled = true;
                dd.propagates -= 1;
              }
              if (type == "drop") {
                ia[ mode ][p] = null;
              }
            }
            // assign any dropinit elements
            else if (type == "dropinit")
              ia.droppable.push(drag.element(result) || subject);
            // accept a returned proxy element
            if (type == "dragstart")
              ia.proxy = $(drag.element(result) || ia.drag)[0];
            // remember this result
            ia.results.push(result);
            // forget the event result, for recycling
            delete event.result;
            // break on cancelled handler
            if (type !== "dropinit")
              return result;
          });
          // flatten the results
          dd.results[ i ] = drag.flatten(ia.results);
          // accept a set of valid drop targets
          if (type == "dropinit")
            ia.droppable = drag.flatten(ia.droppable);
          // locate drop targets
          if (type == "dragstart" && !ia.cancelled)
            callback.update();
        }
        while (++i < len)
        // restore the original event & type
        event.type = orig.type;
        event.originalEvent = orig.event;
        // return all handler results
        return drag.flatten(dd.results);
      },

      // extend the callback object with drag/drop properties...
      properties: function (event, dd, ia) {
        var obj = ia.callback;
        // elements
        obj.drag = ia.drag;
        obj.proxy = ia.proxy || ia.drag;
        // starting mouse position
        obj.startX = dd.pageX;
        obj.startY = dd.pageY;
        // current distance dragged
        obj.deltaX = event.pageX - dd.pageX;
        obj.deltaY = event.pageY - dd.pageY;
        // original element position
        obj.originalX = ia.offset.left;
        obj.originalY = ia.offset.top;
        // adjusted element position
        obj.offsetX = obj.originalX + obj.deltaX;
        obj.offsetY = obj.originalY + obj.deltaY;
        // assign the drop targets information
        obj.drop = drag.flatten(( ia.drop || [] ).slice());
        obj.available = drag.flatten(( ia.droppable || [] ).slice());
        return obj;
      },

      // determine is the argument is an element or jquery instance
      element: function (arg) {
        if (arg && ( arg.jquery || arg.nodeType == 1 ))
          return arg;
      },

      // flatten nested jquery objects and arrays into a single dimension array
      flatten: function (arr) {
        return $.map(arr, function (member) {
          return member && member.jquery ? $.makeArray(member) :
              member && member.length ? drag.flatten(member) : member;
        });
      },

      // toggles text selection attributes ON (true) or OFF (false)
      textselect: function (bool) {
        $(document)[ bool ? "unbind" : "bind" ]("selectstart", drag.dontstart)
          .css("MozUserSelect", bool ? "" : "none");
        // .attr("unselectable", bool ? "off" : "on" )
        document.unselectable = bool ? "off" : "on";
      },

      // suppress "selectstart" and "ondragstart" events
      dontstart: function () {
        return false;
      },

      // a callback instance contructor
      callback: function () {
      }

    };

// callback methods
  drag.callback.prototype = {
    update: function () {
      if ($special.drop && this.available.length)
        $.each(this.available, function (i) {
          $special.drop.locate(this, i);
        });
    }
  };

// patch $.event.$dispatch to allow suppressing clicks
  var $dispatch = $event.dispatch;
  $event.dispatch = function (event) {
    if ($.data(this, "suppress." + event.type) - new Date().getTime() > 0) {
      $.removeData(this, "suppress." + event.type);
      return;
    }
    return $dispatch.apply(this, arguments);
  };

// event fix hooks for touch events...
  var touchHooks =
    $event.fixHooks.touchstart =
      $event.fixHooks.touchmove =
        $event.fixHooks.touchend =
          $event.fixHooks.touchcancel = {
            props: "clientX clientY pageX pageY screenX screenY".split(" "),
            filter: function (event, orig) {
              if (orig) {
                var touched = ( orig.touches && orig.touches[0] )
                  || ( orig.changedTouches && orig.changedTouches[0] )
                  || null;
                // iOS webkit: touchstart, touchmove, touchend
                if (touched)
                  $.each(touchHooks.props, function (i, prop) {
                    event[ prop ] = touched[ prop ];
                  });
              }
              return event;
            }
          };

// share the same special event configuration with related events...
  $special.draginit = $special.dragstart = $special.dragend = drag;

})(jQuery);
